package cn.hg.solon.youcan.framework.web.filter;

import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Map;

import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtMethod;
import org.apache.ibatis.javassist.NotFoundException;
import org.dromara.hutool.core.date.DateUtil;
import org.dromara.hutool.core.reflect.ClassUtil;
import org.dromara.hutool.core.text.CharSequenceUtil;
import org.dromara.hutool.core.text.StrValidator;
import org.dromara.hutool.core.util.ObjUtil;
import org.dromara.hutool.http.meta.HttpStatus;
import org.dromara.hutool.json.JSONUtil;
import org.noear.solon.Solon;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.ModelAndView;
import org.noear.solon.core.route.RouterHandler;
import org.noear.solon.core.route.RouterInterceptor;
import org.noear.solon.core.util.RankEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import cn.hg.solon.youcan.common.util.ConsoleColor;

/**
 * @author 胡高
 * @see io.jboot.web.handler.JbootActionReporter
 */
public class ActionReporter {

    private static class SystemOutWriter extends Writer {
        @Override
        public void close() throws IOException {}

        @Override
        public void flush() throws IOException {}

        @Override
        public void write(char[] cbuf, int off, int len) throws IOException {}

        @Override
        public void write(String str) throws IOException {
            log.debug(str);
        }
    }

    private static final String title = "\nSolon-" + Solon.version() + " action report -------- ";
    private static final String interceptMethodDesc = "(Lorg/noear/solon/core/handle/Context;Lorg/noear/solon/core/handle/Handler;Lorg/noear/solon/core/route/RouterInterceptorChain;)V";
    private static int maxOutputLengthOfParaValue = 128;
    private static Writer writer = new SystemOutWriter();
    private static boolean reportEnable = Solon.cfg().isDebugMode();
    private static boolean colorRenderEnable = true;
    private static boolean reportAllText = false;

    protected static Logger log = LoggerFactory.getLogger(ActionReporter.class);

    private static void doReport(Context ctx, long time) throws NotFoundException, IOException {
        if (ObjUtil.isNull(ctx.action())) {
            return;
        }

        StringBuilder sb = new StringBuilder(title).append(DateUtil.formatDateTime(DateUtil.now()))
            .append(" -------------------------\n");

        doReportRequest(sb, ctx);

        doReportController(sb, ctx);

        doReportMethod(sb, ctx);

        doReportUrlParas(sb, ctx);

        doReportInterceptors(sb, ctx);

        doReportParameters(sb, ctx);

        doReportRender(sb, ctx);

        sb.append("----------------------------------- taked " + time + " ms --------------------------------\n\n\n");

        writer.write(sb.toString());
    }

    private static void doReportController(StringBuilder sb, Context ctx) throws NotFoundException {
        CtClass ctClass = ClassPool.getDefault().get(ctx.action().controller().get().getClass().getName());
        ClassPool.getDefault().get(ctx.action().controller().get().getClass().getName());
        String desc = ActionReporterUtil.getMethodDescWithoutName(ctx.action().method().getMethod());
        CtMethod ctMethod = ctClass.getMethod(ctx.action().method().getName(), desc);
        int lineNumber = ctMethod.getMethodInfo().getLineNumber(0);

        Class<?> cc = ctx.action().controller().raw().getClass();
        sb.append("Controller  : ").append(cc.getName()).append(".(").append(getClassFileName(cc)).append(".java:").append(lineNumber).append(")");
        if (ctx.getHandled()) {
            sb.append((colorRenderEnable ? ConsoleColor.GREEN_BRIGHT : "") + " ---> invoked √"
                + (colorRenderEnable ? ConsoleColor.RESET : ""));
        } else {
            sb.append((colorRenderEnable ? ConsoleColor.RED_BRIGHT : "") + " ---> skipped ×"
                + (colorRenderEnable ? ConsoleColor.RESET : ""));
        }

    }

    private static void doReportInterceptors(StringBuilder sb, Context ctx) throws NotFoundException {
        List<RankEntity<RouterInterceptor>> invokedInterceptors = Solon.app().chainManager().getInterceptorNodes();

        if (invokedInterceptors.size() > 0) {
            sb.append("Interceptor : ");
            for (int i = 0; i < invokedInterceptors.size(); i++) {
                RouterInterceptor inter = invokedInterceptors.get(i).target;
                Class<?> interClass = ClassUtil.getClass(inter);

                // 跳过Solon内置的Handler
                if (interClass.equals(RouterHandler.class)) {
                    continue;
                }

                if (i > 0) {
                    sb.append("\n              ");
                }

                CtClass icClass = ClassPool.getDefault().get(interClass.getName());
                CtMethod icMethod = icClass.getMethod("doIntercept", interceptMethodDesc);
                int icLineNumber = icMethod.getMethodInfo().getLineNumber(0);
                sb.append(icMethod.getDeclaringClass().getName())
                .append(".(")
                .append(icMethod.getDeclaringClass().getSimpleName())
                .append(".java:")
                .append(icLineNumber)
                .append(")");
            }
            sb.append("\n");
        }

    }

    private static void doReportMethod(StringBuilder sb, Context ctx) {
        sb.append("\nMethod      : ").append(ActionReporterUtil.getMethodString(ctx.action().method().getMethod()))
        .append("\n");
    }

    private static void doReportParameters(StringBuilder sb, Context ctx) throws IOException {
        // print all parameters
        Map<String, List<String>> e = ctx.paramsMap();
        if (e.size() > 0) {
            sb.append("Parameter   : ");
            for (String key : e.keySet()) {
                List<String> values = e.get(key);
                if (values.size() == 1) {
                    sb.append(key).append("=");
                    if (values.get(0) != null && values.get(0).length() > maxOutputLengthOfParaValue) {
                        sb.append(values.get(0), 0, maxOutputLengthOfParaValue).append("...");
                    } else {
                        sb.append(values.get(0));
                    }
                } else {
                    sb.append(key).append("[]={");
                    for (int i = 0; i < values.size(); i++) {
                        if (i > 0) {
                            sb.append(",");
                        }
                        sb.append(values.get(0));
                    }
                    sb.append("}");
                }
                sb.append("  ");
            }
            sb.append('\n');
        }

        if (!"GET".equalsIgnoreCase(ctx.method()) && !ctx.isMultipartFormData()
            && StrValidator.isNotBlank(ctx.body())) {
            sb.append("RawData     : ").append(ctx.body());
            sb.append("\n");
        }
    }

    private static void doReportRender(StringBuilder sb, Context ctx) {
        Class<?> retType = ctx.action().method().getMethod().getReturnType();

        if (retType == null) {
            return;
        }

        if (HttpStatus.isRedirected(ctx.status())) {
            String url = "[UNKNOWN]"; // TODO: 目前Solon没有获取输出Header的能力，所以暂时输出未知
            sb.append("Redirect    : ").append(url);
        } else if (ClassUtil.isAssignable(retType, ModelAndView.class)) {
            sb.append("Render View : ").append(ObjUtil.isNull(ctx.result) ? "[null]" : ((ModelAndView)ctx.result).view());
        } else if (ClassUtil.isAssignable(retType, String.class)) {
            sb.append("Render Text : ").append(getRenderText((String)ctx.result));
        } else {
            String jsonStr = JSONUtil.toJsonStr(ctx.result);
            if (CharSequenceUtil.length(jsonStr) > maxOutputLengthOfParaValue) {
                jsonStr = CharSequenceUtil.subPre(jsonStr, maxOutputLengthOfParaValue) + "...";
            }
            sb.append("Render Json : ").append(jsonStr);
        }
        sb.append("\n");
    }

    private static void doReportRequest(StringBuilder sb, Context ctx) {
        sb.append("Request     : ").append(ctx.method()).append(" ").append(ctx.pathNew()).append("\n");
    }

    private static void doReportUrlParas(StringBuilder sb, Context ctx) {
        String urlParas = ctx.queryString();
        if (urlParas != null) {
            sb.append("UrlPara     : ").append(urlParas).append("\n");
        }
    }

    private static String getClassFileName(Class<?> clazz) {
        String classFileName = clazz.getName();
        if (classFileName.contains("$")) {
            int indexOf = classFileName.contains(".") ? classFileName.lastIndexOf(".") + 1 : 0;
            return classFileName.substring(indexOf, classFileName.indexOf("$"));
        }
        return clazz.getSimpleName();
    }

    private static String getRenderText(String orignalText) {
        if (StrValidator.isBlank(orignalText)) {
            return "";
        }

        if (!reportAllText && orignalText.length() > maxOutputLengthOfParaValue) {
            return orignalText.substring(0, maxOutputLengthOfParaValue) + "...";
        }

        return orignalText;
    }

    public static Writer getWriter() {
        return writer;
    }

    public static boolean isColorRenderEnable() {
        return colorRenderEnable;
    }

    public static boolean isReportAllText() {
        return reportAllText;
    }

    public static boolean isReportEnable() {
        return reportEnable;
    }

    public static void report(Context ctx, long time) {
        if (!reportEnable) {
            return;
        }

        try {
            doReport(ctx, time);
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
        }
    }

    public static void setColorRenderEnable(boolean colorRenderEnable) {
        ActionReporter.colorRenderEnable = colorRenderEnable;
    }

    public static void setMaxOutputLengthOfParaValue(int maxOutputLengthOfParaValue) {
        if (maxOutputLengthOfParaValue < 16) {
            throw new IllegalArgumentException("maxOutputLengthOfParaValue must more than 16");
        }
        ActionReporter.maxOutputLengthOfParaValue = maxOutputLengthOfParaValue;
    }

    public static void setReportAllText(boolean reportAllText) {
        ActionReporter.reportAllText = reportAllText;
    }

    public static void setReportEnable(boolean reportEnable) {
        ActionReporter.reportEnable = reportEnable;
    }

    public static void setWriter(Writer writer) {
        if (writer == null) {
            throw new IllegalArgumentException("writer can not be null");
        }
        ActionReporter.writer = writer;
    }
}
